﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Skugo.Core;
using Skugo.Shared;
using System.Diagnostics;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows;
using System.Globalization;
using System.Reflection;
using System.Windows.Media.Imaging;
using System.Collections.ObjectModel;

namespace Skugo.StandardPlugins
{
    public class Command
    {
        public CommandBinding Binding;
        public String InputGestureText;
        public HashSet<String> Keywords = new HashSet<String>();
        public MenuItem MenuItem = null;
        public String Name { get; set; }
        public String Description { get; set; }
    }

    public class CommandManager : Plugin
    {
        /// <summary>
        /// The type of message we send or receieve.
        /// </summary>
        enum MessageType : byte
        {
            CreateCommand,
            DestroyCommand,
            CommandExecuted,
        }

        /// <summary>
        /// Maps command names to their given command (mostly so they can be destroyed or debugged).
        /// </summary>
        internal Dictionary<String, Command> Commands = new Dictionary<String, Command>();

        /// <summary>
        /// Our categories act as base/root level menu items.
        /// </summary>
        private Dictionary<String, MenuItem> Categories = new Dictionary<String, MenuItem>();

        /// <summary>
        /// The command menu is an automatically generated menu bar at the top.
        /// </summary>
        private Menu CommandMenu;

        private CommandBinding SearchBinding;

        protected override void Initialize(BinaryReader incoming)
        {
            base.Initialize(incoming);

            // Create the shared menu
            this.CommandMenu = new Menu();
            this.CommandMenu.IsMainMenu = true;

            // Set the docking mode
            DockPanel.SetDock(this.CommandMenu, Dock.Top);

            // Add the menu to the framework as the first
            // element so it always stretches accross the top dock
            this.Client.Dock.Children.Insert(0, this.CommandMenu);

            // Create the keyboard shortcut / gesture for the command window
            var shortcut = new RoutedCommand();
            var gesture = new KeyGesture(System.Windows.Input.Key.E, ModifierKeys.Control);
            shortcut.InputGestures.Add(gesture);

            // Create the delegate that is invoked when the command is invoked
            this.SearchBinding = new CommandBinding(shortcut, (sender, e) =>
            {
                // Create the command search window
                var search = new CommandSearch(this);
                search.Owner = Framework.Main;
                search.Show();
            });

            // Add the command binding to the 
            Framework.Main.CommandBindings.Add(this.SearchBinding);
        }

        internal void ExecuteRemoteCommand(String name)
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the frame buffer message
            outgoing.Write((Byte)MessageType.CommandExecuted);

            // Write all command setup information
            outgoing.WriteAsciiString(name);

            // Send the packet
            this.Send(stream.ToArray());
        }

        protected override void OnReceive(Byte[] data)
        {
            // Create a reader to read the data
            var incoming = data.ToBinaryReader();

            // Read the message type in
            var messageType = (MessageType)incoming.ReadByte();

            // Based on the message type...
            switch (messageType)
            {
                case MessageType.CreateCommand:
                {
                    // Read the all of the properties of the command (including the name)
                    var name = incoming.ReadAsciiString();
                    var description = incoming.ReadAsciiString();
                    var category = incoming.ReadAsciiString();
                    var icon = (Icon)incoming.ReadInt32();
                    var key = (Skugo.Shared.Key)incoming.ReadInt32();
                    var modifiers = (Skugo.Shared.KeyModifiers)incoming.ReadInt32();

                    // Read the number of keywords this command has
                    var keywordCount = incoming.ReadInt32();

                    // Create a skugo command
                    var command = new Command();

                    // Set the name of the command and its description
                    command.Name = name;
                    command.Description = description;

                    // Loop through all the command's keywords
                    for (int i = 0; i < keywordCount; ++i)
                    {
                        // Add the keyword to our created command
                        command.Keywords.Add(incoming.ReadAsciiString());
                    }

                    // Now, if we have a keyboard key set...
                    if (key != Shared.Key.None)
                    {
                        // Create the keyboard shortcut / gesture
                        var shortcut = new RoutedCommand();
                        var gesture = new KeyGesture((System.Windows.Input.Key)key, (ModifierKeys)modifiers);
                        shortcut.InputGestures.Add(gesture);

                        // Create the delegate that is invoked when the command is invoked
                        command.Binding = new CommandBinding(shortcut, (sender, e) =>
                        {
                            this.ExecuteRemoteCommand(name);
                        });

                        // Create input gesture text
                        command.InputGestureText = gesture.GetDisplayStringForCulture(CultureInfo.CurrentCulture);

                        // Add the command binding to the 
                        Framework.Main.CommandBindings.Add(command.Binding);
                    }

                    // If we have a category set...
                    if (category != String.Empty)
                    {
                        // Attempt to get the category menu from the map...
                        MenuItem categoryMenu;
                        if (this.Categories.TryGetValue(category, out categoryMenu) == false)
                        {
                            // Create the category menu and add it to the command menu
                            categoryMenu = new MenuItem();
                            categoryMenu.Header = category;
                            categoryMenu.Name = category;
                            this.CommandMenu.Items.Add(categoryMenu);

                            // Add that menu to our list of categories
                            this.Categories.Add(category, categoryMenu);
                        }

                        // Create the command menu item
                        var commandMenuItem = new MenuItem();
                        commandMenuItem.Header = name;
                        commandMenuItem.Name = name;

                        // If we have a description...
                        if (description != String.Empty)
                        {
                            // If we have input gesture text...
                            if (command.InputGestureText != null)
                            {
                                // Concatenate them (so toolbars will show the tooltip and shortcut)
                                commandMenuItem.ToolTip = description + " (" + command.InputGestureText + ")";
                            } 
                            else
                            {
                                // Just show the description
                                commandMenuItem.ToolTip = description;
                            }
                        }

                        // Associate the keyboard binding
                        commandMenuItem.Click += (sender, e) =>
                        {
                            this.ExecuteRemoteCommand(name);
                        };

                        // If we have an icon
                        if (icon != Icon.None)
                        {
                            // Load the icon as a resource
                            commandMenuItem.Icon = new Image()
                            {
                                Source = IconLoader.Load(icon),
                                Width = 16,
                                Height = 16,
                            };
                        }

                        // If we have a keyboard binding (shortcut)...
                        if (command.InputGestureText != null)
                        {
                            // Show the shortcut keys
                            commandMenuItem.InputGestureText = command.InputGestureText;
                        }

                        // Add the item to the category menu
                        categoryMenu.Items.Add(commandMenuItem);
                    }

                    // Add the command to the map
                    this.Commands.Add(name, command);
                    break;
                }

                case MessageType.DestroyCommand:
                {
                    // Read the name of the command we want to destroy
                    var name = incoming.ReadAsciiString();

                    // Find the command by name
                    var command = this.Commands[name];

                    // Dispose of the command and remove it from our list
                    this.Commands.Remove(name);
                    Framework.Main.CommandBindings.Remove(command.Binding);

                    // Grab the menu item's parent
                    var parent = (ItemsControl)command.MenuItem.Parent;

                    // Remove the menu item from its parent
                    parent.Items.Remove(command.MenuItem);

                    // If the parent no longer has any items in it...
                    if (parent.Items.Count == 0)
                    {
                        // Remove the parent category (and remove it from the main menu too)
                        this.Categories.Remove(parent.Name);
                        ((ItemsControl)parent.Parent).Items.Remove(parent);
                    }

                    break;
                }

                default:
                    throw new Exception("Unhandled message type!");
            }
        }

        public override void Dispose()
        {
            // Add the menu to the framework
            this.Client.Dock.Children.Remove(this.CommandMenu);

            Framework.Main.CommandBindings.Remove(this.SearchBinding);

            // Remove all the bindings we've created
            foreach (var command in this.Commands.Values)
            {
                Framework.Main.CommandBindings.Remove(command.Binding);
            }
        }
    }
}